P02-2. 1세대 Sidecar 메시의 커널 내부 해부 - 실습 검증
Part 02 (실습): 1세대 Sidecar 메시의 커널 내부 해부 — 실습 검증
- 쿠버네티스 API 서버에 등록된
MutatingWebhookConfiguration(istio-sidecar-injector) 과 파드 내부에 주입된istio-init초기화 컨테이너,istio-proxy사이드카(UID 1337) 설정을 직접 눈으로 확인한다. istio-init컨테이너 로그를 열어 파드 생성 시점에 실행된iptables-nft-restore의 전체 룰셋(PREROUTING,OUTPUT,ISTIO_INBOUND,ISTIO_OUTPUT)을 해부한다.- 워커 노드(
desktop-worker,desktop-worker2)에서 리눅스nsenter도구로 파드의 네트워크 네임스페이스에 직접 침투해, 실시간 패킷 카운터(pkts)로15001납치 →UID 1337예외 통과 →15006납치 →127.0.0.6루프백 통과 과정을 100% 실측 증명한다.
- P02-1 이론 개념을 먼저 읽고 오면 각 포트(
15001,15006,15021)와 IP(127.0.0.6), UID(1337)의 역할이 훨씬 선명하게 보입니다. - Part 01 실습을 마친 상태(
docker-desktop3노드 클러스터에Istio 1.31.3및part1-demo네임스페이스의order-api,study-client파드가READY 2/2로 실행 중인 상태)에서 바로 이어서 진행합니다. - Windows PowerShell 창을 열고 아래 명령어로 현재 파드 상태를 먼저 점검합니다.
$env:Path += ";$HOME\bin" cd "$HOME\mesh-study" kubectl get pods -n part1-demo -o wide
실습 아키텍처 및 검증 포인트 한눈에 보기
이번 실습에서 우리가 파헤칠 3가지 검문소의 위치입니다.
flowchart TB
subgraph K8S_API["검증 1 · K8s API Server (파드 생성 시점)"]
MW["MutatingWebhookConfiguration
(istio-sidecar-injector)"] -->|"YAML 자동 개조"| POD_SPEC["Pod Spec에 2개 컨테이너 주입:
1. initContainers: istio-init (NET_ADMIN)
2. containers: istio-proxy (UID 1337)"]
end
subgraph CLIENT_NETNS["검증 2 · study-client 파드 NetNS (desktop-worker2)"]
C_CURL["curl 프로세스 (UID 100)"] -->|"OUTPUT 체인"| C_NAT["ISTIO_REDIRECT
(--to-ports 15001, pkts 증가!)"]
C_NAT --> C_ENVOY["Envoy (UID 1337)"]
C_ENVOY -->|"UID 1337 매칭"| C_RET["ISTIO_OUTPUT RETURN
(무한 루프 탈출, pkts 증가!)"]
end
subgraph SERVER_NETNS["검증 3 · order-api 파드 NetNS (desktop-worker)"]
S_PREROUTE["PREROUTING 체인"] -->|"포트 15021 헬스체크"| S_HEALTH["RETURN (kubelet 직통)"]
S_PREROUTE -->|"일반 트래픽 (:8080)"| S_IN["ISTIO_IN_REDIRECT
(--to-ports 15006, pkts 증가!)"]
S_IN --> S_ENVOY["Envoy (UID 1337)"]
S_ENVOY -->|"출발지 IP 127.0.0.6 변환"| S_LOOP["ISTIO_OUTPUT RETURN
(-s 127.0.0.6 -o lo, pkts 증가!)"]
end
C_RET -->|"노드 간 mTLS 통신"| S_PREROUTEStep 1. 파드 자동 개조 범인 색출 (MutatingWebhook & initContainers)
Part 01에서 우리가 작성한 order-api.yaml에는 컨테이너가 1개뿐이었는데, 어떻게 파드 안에 컨테이너가 2개(2/2)로 불어났는지 쿠버네티스 API 서버의 웹훅 설정과 파드 상세 명세를 조회해 확인합니다.
# 1. 클러스터에 등록된 사이드카 자동 주입 웹훅(MutatingWebhookConfiguration) 조회
kubectl get mutatingwebhookconfigurations istio-sidecar-injector
# 2. study-client 파드에 몰래 주입된 초기화 컨테이너(istio-init)와 사이드카(istio-proxy) 설정 추출
kubectl get pod study-client -n part1-demo -o jsonpath="InitContainer: {.spec.initContainers[0].name} | Image: {.spec.initContainers[0].image}`nArgs: {.spec.initContainers[0].args}`nCapabilities: {.spec.initContainers[0].securityContext.capabilities.add}`nProxy Container: {.spec.containers[1].name} | UID: {.spec.containers[1].securityContext.runAsUser}`n"
실제 출력 결과:
NAME WEBHOOKS AGE
istio-sidecar-injector 4 5m4s
InitContainer: istio-init | Image: docker.io/istio/proxyv2:1.31.3
Args: ["istio-iptables","-p","15001","-z","15006","-u","1337","-m","REDIRECT","-i","*","-x","","-b","*","-d","15090,15021,15020","--log_output_level=default:info"]
Capabilities: ["NET_ADMIN","NET_RAW"]
Proxy Container: istio-proxy | UID: 1337
출력 결과 상세 해부:
istio-sidecar-injector: 쿠버네티스 API 서버에 등록된 변이 웹훅(Mutating Webhook) 입니다.istio-injection=enabled라벨이 붙은 네임스페이스에 파드가 생성될 때마다istiod가 개입해 아래 두 컨테이너를 끼워 넣습니다.InitContainer: istio-init:- 역할: 본 컨테이너(
study-client,istio-proxy)가 켜지기 전에 가장 먼저 실행되어 파드의 커널iptables규칙만 세팅하고 종료(Completed)되는 일회용 초기화 컨테이너입니다. Capabilities: ["NET_ADMIN","NET_RAW"]: 리눅스 커널 네트워크 방화벽(iptables)을 조작할 수 있는 관리자 특권입니다.Args옵션 해부:"-p", "15001": 파드 밖으로 나가는(Outbound) 모든 TCP 패킷을15001번 포트로 납치하라!"-z", "15006": 파드 안으로 들어오는(Inbound) 모든 TCP 패킷을15006번 포트로 납치하라!"-u", "1337": 단, 사용자 번호(UID)가1337번인 프로세스가 쏘는 패킷은 납치하지 말고 통과시켜라!"-d", "15090,15021,15020": 들어오는 패킷 중15090(Prometheus 메트릭),15021(헬스체크),15020(통합 메트릭/디버그) 포트로 향하는 패킷도 납치하지 말고 그대로 통과시켜라!
- 역할: 본 컨테이너(
Proxy Container: istio-proxy | UID: 1337:- 실제로 패킷을 받아 처리하는 Envoy 사이드카 컨테이너이며, 정확히
runAsUser: 1337로 실행되고 있음을 확인할 수 있습니다.
- 실제로 패킷을 받아 처리하는 Envoy 사이드카 컨테이너이며, 정확히
컨테이너 내부 프로세스의 실제 UID 대조
정말로 애플리케이션 컨테이너와 Envoy 컨테이너의 리눅스 사용자 ID(UID)가 다른지 두 컨테이너 내부에서 각각 id 명령어를 실행해 비교해 봅니다.
# 1. Envoy 사이드카 컨테이너(istio-proxy)의 리눅스 사용자 ID 확인
kubectl exec study-client -n part1-demo -c istio-proxy -- id
# 2. 원래 애플리케이션 컨테이너(study-client)의 리눅스 사용자 ID 확인
kubectl exec study-client -n part1-demo -c study-client -- id
실제 출력 결과:
uid=1337(istio-proxy) gid=1337(istio-proxy) groups=1337(istio-proxy)
uid=100(curl_user) gid=101(curl_group) groups=101(curl_group)
study-client컨테이너 안에서 실행하는curl명령어는uid=100(curl_user)신분으로 패킷을 만듭니다 (1337번이 아니므로 100% 납치 대상!).- 반면
istio-proxy컨테이너의 Envoy는uid=1337(istio-proxy)신분으로 패킷을 만듭니다 (1337번이므로 납치 면제!).
Step 2. istio-init 로그로 보는 전체 iptables 설계도
초기화 컨테이너인 istio-init은 작업을 마치고 이미 종료되었지만, kubectl logs -c istio-init 명령어를 치면 파드가 태어날 때 커널에 어떤 iptables 명령어를 집어넣었는지 전체 기록을 그대로 볼 수 있습니다.
# istio-init 초기화 컨테이너가 남긴 로그 확인
kubectl logs study-client -n part1-demo -c istio-init
실제 출력 결과 (핵심 발췌):
2026-10-25T06:51:25.998561Z info Istio config:
PROXY_PORT=15001
PROXY_INBOUND_CAPTURE_PORT=15006
PROXY_TUNNEL_PORT=15008
PROXY_UID=1337
PROXY_GID=1337
INBOUND_INTERCEPTION_MODE=REDIRECT
INBOUND_PORTS_INCLUDE=*
INBOUND_PORTS_EXCLUDE=15090,15021,15020
...
2026-10-25T06:51:26.013278Z info Final iptables state (IPv4):
# Generated by iptables-nft-save v1.8.10 (nf_tables) on Sun Oct 25 06:51:26 2026
*nat
:PREROUTING ACCEPT [0:0]
:INPUT ACCEPT [0:0]
:OUTPUT ACCEPT [0:0]
:POSTROUTING ACCEPT [0:0]
:ISTIO_INBOUND - [0:0]
:ISTIO_IN_REDIRECT - [0:0]
:ISTIO_OUTPUT - [0:0]
:ISTIO_REDIRECT - [0:0]
-A PREROUTING -p tcp -j ISTIO_INBOUND
-A OUTPUT -j ISTIO_OUTPUT
-A ISTIO_INBOUND -p tcp -m tcp --dport 15008 -j RETURN
-A ISTIO_INBOUND -p tcp -m tcp --dport 15090 -j RETURN
-A ISTIO_INBOUND -p tcp -m tcp --dport 15021 -j RETURN
-A ISTIO_INBOUND -p tcp -m tcp --dport 15020 -j RETURN
-A ISTIO_INBOUND -p tcp -j ISTIO_IN_REDIRECT
-A ISTIO_IN_REDIRECT -p tcp -j REDIRECT --to-ports 15006
-A ISTIO_OUTPUT -s 127.0.0.6/32 -o lo -j RETURN
-A ISTIO_OUTPUT ! -d 127.0.0.1/32 -o lo -p tcp -m tcp ! --dport 15008 -m owner --uid-owner 1337 -j ISTIO_IN_REDIRECT
-A ISTIO_OUTPUT -o lo -m owner ! --uid-owner 1337 -j RETURN
-A ISTIO_OUTPUT -m owner --uid-owner 1337 -j RETURN
-A ISTIO_OUTPUT ! -d 127.0.0.1/32 -o lo -p tcp -m tcp ! --dport 15008 -m owner --gid-owner 1337 -j ISTIO_IN_REDIRECT
-A ISTIO_OUTPUT -o lo -m owner ! --gid-owner 1337 -j RETURN
-A ISTIO_OUTPUT -m owner --gid-owner 1337 -j RETURN
-A ISTIO_OUTPUT -d 127.0.0.1/32 -j RETURN
-A ISTIO_OUTPUT -j ISTIO_REDIRECT
-A ISTIO_REDIRECT -p tcp -j REDIRECT --to-ports 15001
COMMIT
Final iptables state 18줄이 1세대 사이드카 메시의 전부입니다!P02-1 이론 노트에서 배운 규칙들이 한 줄도 빠짐없이 그대로 들어 있습니다:
-A PREROUTING -p tcp -j ISTIO_INBOUND: 밖에서 들어오는 모든 TCP 패킷은ISTIO_INBOUND로 보내라.--dport 15008 / 15090 / 15021 / 15020 -j RETURN: 헬스체크와 메트릭 포트는 납치하지 말고 통과시켜라.-A ISTIO_IN_REDIRECT -p tcp -j REDIRECT --to-ports 15006: 나머지 들어오는 패킷은 전부 Envoy의15006번 포트로 꺾어라.-A OUTPUT -j ISTIO_OUTPUT: 안에서 나가는 모든 패킷은ISTIO_OUTPUT으로 보내라.-A ISTIO_OUTPUT -s 127.0.0.6/32 -o lo -j RETURN: Envoy(15006)가 검문을 마치고 내부 앱 컨테이너로 넘겨줄 때 쓰는127.0.0.6출발지 패킷은 다시 납치하지 말고 통과시켜라.-A ISTIO_OUTPUT -m owner --uid-owner 1337 -j RETURN: Envoy(UID 1337)가 직접 밖으로 내보내는 패킷은 납치하지 말고 진짜 랜카드 밖으로 내보내라.-A ISTIO_REDIRECT -p tcp -j REDIRECT --to-ports 15001: 나머지 일반 앱(curl등)이 밖으로 보내는 패킷은 전부 Envoy의15001번 포트로 꺾어라.
Step 3. 커널 NetNS 침투(nsenter) — 나가는 패킷(Outbound) 실시간 카운터 검증
로그로 설계도를 봤으니, 이제 진짜로 트래픽을 쏘았을 때 커널 내부에서 패킷이 어느 규칙에 걸려 몇 개(pkts)나 지나갔는지 실시간 패킷 카운터를 찍어 봅니다.
일반 파드 컨테이너 안에는 보안상 iptables 명령어가 없거나 실행 권한(NET_ADMIN)이 없습니다. 하지만 우리는 워커 노드(desktop-worker2)에 docker exec로 접속한 뒤, 리눅스의 nsenter(Namespace Enter) 도구로 파드의 네트워크 방(NetNS)만 살짝 열고 들어가서 커널 테이블을 직접 조회할 수 있습니다!
nsenter 와 pidof
pidof <프로세스이름>: 현재 리눅스 머신에서 특정 프로그램(예:sleep또는node)이 몇 번 프로세스 번호(PID)로 돌고 있는지 찾아주는 명령어입니다.nsenter -t <PID> -n <명령어>: "특정 프로세스(-t <PID>)가 속해 있는 네트워크 네임스페이스 방(-n) 안으로 들어가서 뒤의 명령어를 실행하라"는 리눅스 시스템 관리자 도구입니다. 워커 노드의 관리자 권한을 유지한 채 파드의 네트워크 방 안쪽 상태만 들여다볼 수 있어 실무 트러블슈팅에서 가장 강력한 무기입니다.
# 1. study-client에서 order-api로 HTTP 요청 1회 전송
kubectl exec -n part1-demo study-client -c study-client -- curl -s http://order-api/healthz | Out-Null
# 2. study-client가 떠 있는 워커 노드 이름 확인
$clientNode = kubectl get pod study-client -n part1-demo -o jsonpath="{.spec.nodeName}"
Write-Output "study-client is on node: $clientNode"
# 3. 해당 워커 노드에서 sleep 프로세스(study-client)의 NetNS로 들어가 실시간 iptables 패킷 카운터 조회
docker exec $clientNode sh -c 'nsenter -t $(pidof sleep) -n iptables -t nat -L -n -v'
실제 출력 결과:
study-client is on node: desktop-worker2
# Warning: iptables-legacy tables present, use iptables-legacy to see them
Chain PREROUTING (policy ACCEPT 16 packets, 960 bytes)
pkts bytes target prot opt in out source destination
16 960 ISTIO_INBOUND 6 -- * * 0.0.0.0/0 0.0.0.0/0
Chain INPUT (policy ACCEPT 16 packets, 960 bytes)
pkts bytes target prot opt in out source destination
Chain OUTPUT (policy ACCEPT 24 packets, 1990 bytes)
pkts bytes target prot opt in out source destination
25 2050 ISTIO_OUTPUT 0 -- * * 0.0.0.0/0 0.0.0.0/0
Chain POSTROUTING (policy ACCEPT 0 packets, 0 bytes)
pkts bytes target prot opt in out source destination
Chain ISTIO_INBOUND (1 references)
pkts bytes target prot opt in out source destination
0 0 RETURN 6 -- * * 0.0.0.0/0 0.0.0.0/0 tcp dpt:15008
0 0 RETURN 6 -- * * 0.0.0.0/0 0.0.0.0/0 tcp dpt:15090
16 960 RETURN 6 -- * * 0.0.0.0/0 0.0.0.0/0 tcp dpt:15021
0 0 RETURN 6 -- * * 0.0.0.0/0 0.0.0.0/0 tcp dpt:15020
0 0 ISTIO_IN_REDIRECT 6 -- * * 0.0.0.0/0 0.0.0.0/0
Chain ISTIO_IN_REDIRECT (3 references)
pkts bytes target prot opt in out source destination
0 0 REDIRECT 6 -- * * 0.0.0.0/0 0.0.0.0/0 redir ports 15006
Chain ISTIO_OUTPUT (1 references)
pkts bytes target prot opt in out source destination
0 0 RETURN 0 -- * lo 127.0.0.6 0.0.0.0/0
0 0 ISTIO_IN_REDIRECT 6 -- * lo 0.0.0.0/0 !127.0.0.1 tcp dpt:!15008 owner UID match 1337
0 0 RETURN 0 -- * lo 0.0.0.0/0 0.0.0.0/0 ! owner UID match 1337
23 1906 RETURN 0 -- * * 0.0.0.0/0 0.0.0.0/0 owner UID match 1337
0 0 ISTIO_IN_REDIRECT 6 -- * lo 0.0.0.0/0 !127.0.0.1 tcp dpt:!15008 owner GID match 1337
0 0 RETURN 0 -- * lo 0.0.0.0/0 0.0.0.0/0 ! owner GID match 1337
0 0 RETURN 0 -- * * 0.0.0.0/0 0.0.0.0/0 owner GID match 1337
0 0 RETURN 0 -- * * 0.0.0.0/0 127.0.0.1
2 144 ISTIO_REDIRECT 0 -- * * 0.0.0.0/0 0.0.0.0/0
Chain ISTIO_REDIRECT (1 references)
pkts bytes target prot opt in out source destination
1 60 REDIRECT 6 -- * * 0.0.0.0/0 0.0.0.0/0 redir ports 15001
pkts)로 읽는 커널 내부의 실제 사건 기록
참고로 iptables -t nat 테이블은 매 패킷마다 검사하는 것이 아니라, 새로운 연결(TCP SYN 또는 첫 UDP 패킷)이 시작될 때 딱 1번만 검사하고 그 결과를 커널 conntrack(연결 추적기)에 기억해 둡니다. 따라서 pkts 숫자는 "새로 맺어진 연결의 횟수" 를 뜻합니다!
Chain ISTIO_REDIRECT→1 pkts / 60 bytes REDIRECT ... redir ports 15001:
방금 우리가 실행한curl(UID 100)이order-api로 연결을 시도한 TCPSYN패킷(60바이트) 1개가 정확히 이 규칙에 걸려 Envoy의15001번 포트로 납치되었습니다!Chain ISTIO_OUTPUT→23 pkts / 1906 bytes RETURN ... owner UID match 1337:
납치된 요청을 넘겨받은 Envoy(UID 1337)가 컨트롤 플레인(istiod)과 통신하거나order-api파드로 진짜 패킷을 내보낼 때 만든 23개의 연결은 모두owner UID match 1337규칙에 걸려 무한 루프에 빠지지 않고 즉시RETURN(통과) 되었습니다!Chain ISTIO_INBOUND→16 pkts / 960 bytes RETURN ... tcp dpt:15021:
워커 노드의kubelet이 이 파드가 살아있는지 확인하려고 몇 초마다 찌른 16번의 헬스체크 연결(:15021) 은 납치되지 않고 바로RETURN(통과) 되었습니다!
Step 4. 들어오는 패킷(Inbound) & 127.0.0.6 루프백 마법 실시간 검증
이번에는 요청을 받는 서버 파드인 order-api 가 떠 있는 desktop-worker 노드로 가서, 들어오는 패킷이 15006번으로 납치된 뒤 127.0.0.6 출발지 주소를 달고 내부 Node.js 서버(:8080)로 들어가는 모습을 포착해 봅니다.
# 1. study-client에서 order-api로 4번 연속 요청 전송 (두 파드에 골고루 요청이 도달하도록)
kubectl exec -n part1-demo study-client -c study-client -- curl -s --http1.1 "http://order-api/test?[1-4]" | Out-Null
# 2. desktop-worker 노드에서 돌고 있는 order-api(node 프로세스)의 NetNS 내부 iptables 카운터 확인
docker exec desktop-worker sh -c 'nsenter -t $(pidof node) -n iptables -t nat -L -n -v'
실제 출력 결과:
# Warning: iptables-legacy tables present, use iptables-legacy to see them
Chain PREROUTING (policy ACCEPT 14 packets, 840 bytes)
pkts bytes target prot opt in out source destination
15 900 ISTIO_INBOUND 6 -- * * 0.0.0.0/0 0.0.0.0/0
Chain INPUT (policy ACCEPT 15 packets, 900 bytes)
pkts bytes target prot opt in out source destination
Chain OUTPUT (policy ACCEPT 23 packets, 1906 bytes)
pkts bytes target prot opt in out source destination
23 1906 ISTIO_OUTPUT 0 -- * * 0.0.0.0/0 0.0.0.0/0
Chain POSTROUTING (policy ACCEPT 0 packets, 0 bytes)
pkts bytes target prot opt in out source destination
Chain ISTIO_INBOUND (1 references)
pkts bytes target prot opt in out source destination
0 0 RETURN 6 -- * * 0.0.0.0/0 0.0.0.0/0 tcp dpt:15008
0 0 RETURN 6 -- * * 0.0.0.0/0 0.0.0.0/0 tcp dpt:15090
14 840 RETURN 6 -- * * 0.0.0.0/0 0.0.0.0/0 tcp dpt:15021
0 0 RETURN 6 -- * * 0.0.0.0/0 0.0.0.0/0 tcp dpt:15020
1 60 ISTIO_IN_REDIRECT 6 -- * * 0.0.0.0/0 0.0.0.0/0
Chain ISTIO_IN_REDIRECT (3 references)
pkts bytes target prot opt in out source destination
1 60 REDIRECT 6 -- * * 0.0.0.0/0 0.0.0.0/0 redir ports 15006
Chain ISTIO_OUTPUT (1 references)
pkts bytes target prot opt in out source destination
1 60 RETURN 0 -- * lo 127.0.0.6 0.0.0.0/0
0 0 ISTIO_IN_REDIRECT 6 -- * lo 0.0.0.0/0 !127.0.0.1 tcp dpt:!15008 owner UID match 1337
0 0 RETURN 0 -- * lo 0.0.0.0/0 0.0.0.0/0 ! owner UID match 1337
22 1846 RETURN 0 -- * * 0.0.0.0/0 0.0.0.0/0 owner UID match 1337
0 0 ISTIO_IN_REDIRECT 6 -- * lo 0.0.0.0/0 !127.0.0.1 tcp dpt:!15008 owner GID match 1337
0 0 RETURN 0 -- * lo 0.0.0.0/0 0.0.0.0/0 ! owner GID match 1337
0 0 RETURN 0 -- * * 0.0.0.0/0 0.0.0.0/0 owner GID match 1337
0 0 RETURN 0 -- * * 0.0.0.0/0 127.0.0.1
0 0 ISTIO_REDIRECT 0 -- * * 0.0.0.0/0 0.0.0.0/0
Chain ISTIO_REDIRECT (1 references)
pkts bytes target prot opt in out source destination
0 0 REDIRECT 6 -- * * 0.0.0.0/0 0.0.0.0/0 redir ports 15001
order-api 커널 카운터에서 증명된 2가지 결정적 장면!
Chain ISTIO_IN_REDIRECT→1 pkts / 60 bytes REDIRECT ... redir ports 15006:
밖에서order-api파드의:8080포트를 향해 들어온 연결(1 pkts)이PREROUTING→ISTIO_INBOUND를 거쳐 정확히 Envoy의 Inbound 포트인15006번으로 납치되었습니다!Chain ISTIO_OUTPUT첫 번째 줄 →1 pkts / 60 bytes RETURN ... lo 127.0.0.6 0.0.0.0/0:
15006번에서 패킷을 받아 mTLS 암호를 풀고 검문을 마친 Envoy(UID 1337)가 같은 파드 안의 Node.js(:8080)에게 패킷을 넘겨줄 때, 출발지 IP를127.0.0.6으로 찍어서 루프백(lo)으로 보냈고, 그 연결(1 pkts)이 정확히ISTIO_OUTPUT의 첫 번째 줄에 걸려 무한 루프 없이RETURN(통과) 되어 Node.js 서버에 무사히 도착했습니다!
Step 5. 왜 1 pkts만 찍혔을까? (Envoy HTTP/2 커넥션 풀 재사용 확인)
방금 우리는 curl -s --http1.1 "http://order-api/test?[1-4]" 로 분명히 4번의 HTTP 요청을 보냈고, 두 개의 order-api 파드가 각각 2번씩 요청을 나누어 받았습니다. 그런데 왜 order-api의 iptables 카운터에는 2 pkts가 아니라 1 pkts 만 찍혔을까요?
이유는 Envoy 프록시가 한 번 맺은 백엔드 연결(TCP/mTLS 터널)을 끊지 않고 커넥션 풀(Connection Pool)에 보관해 두었다가 다음 HTTP 요청 때 그대로 재사용(Keep-Alive / Multiplexing) 하기 때문입니다!
- 첫 번째 요청이 들어올 때만 새 TCP 연결(
SYN)이 맺어지므로iptables -t nat카운터가1올라갑니다. - 두 번째 요청부터는 이미 열려 있는 연결 통로 위로 데이터만 휙 지나가므로 커널의
nat테이블을 다시 거치지 않아 CPU 부하가 획기적으로 줄어듭니다.
Step 6. 자주 발생하는 문제와 해결 (Troubleshooting)
| 증상 / 에러 메시지 | 원인 | 해결 방법 |
|---|---|---|
파드 안에서 kubectl exec ... -- iptables -t nat -L 을 치면 command not found 또는 Permission denied (you must be root) 발생 |
보안 모범 사례(Least Privilege)에 따라 istio-init이 종료된 뒤 실행되는 study-client와 istio-proxy 컨테이너에는 NET_ADMIN 권한이 회수되어 있기 때문 |
본 실습의 Step 3처럼 워커 노드에 docker exec <노드이름>으로 들어간 뒤 nsenter -t $(pidof <프로세스>) -n iptables -t nat -L -n -v로 호스트에서 파드 NetNS를 열람 |
사내 보안 정책(Kyverno / PodSecurity)에서 NET_ADMIN 권한을 막아 파드가 CreateContainerConfigError로 안 뜸 |
istio-init 초기화 컨테이너는 반드시 NET_ADMIN과 NET_RAW 권한이 있어야 iptables를 세팅할 수 있음 |
클러스터에 Istio CNI 플러그인(istio-cni-node DaemonSet)을 설치해 istio-init 대신 노드 CNI 레벨에서 iptables를 세팅하게 하거나, Part 03에서 배울 2세대 Ambient Mesh로 전환 |
요청을 여러 번 보냈는데도 iptables -t nat -L -v의 pkts 숫자가 더 이상 안 올라감 |
iptables의 nat 테이블은 최초 연결 수립(TCP SYN) 시점에만 1회 평가되고, 이후 패킷이나 재사용된 커넥션 풀 연결은 커널 conntrack이 바로 처리하기 때문 |
정상 동작입니다. 카운터가 올라가는 것을 다시 보고 싶다면 잠시 기다려 유휴 커넥션이 닫히게 하거나 새로운 대상 포트로 요청을 보내면 됩니다. |
Step 7. 이번 파트 핵심 명령어 정리 (Command Reference)
# 1. 사이드카 자동 주입 웹훅(MutatingWebhookConfiguration) 확인
kubectl get mutatingwebhookconfigurations istio-sidecar-injector
# 2. 파드 생성 시 istio-init 초기화 컨테이너가 세팅한 전체 iptables-nft 규칙 로그 확인
kubectl logs <파드이름> -n <네임스페이스> -c istio-init
# 3. 컨테이너별 리눅스 사용자 번호(UID) 확인 (Envoy는 1337번)
kubectl exec <파드이름> -n <네임스페이스> -c istio-proxy -- id
# 4. 워커 노드에서 nsenter로 파드 NetNS 내부의 실시간 iptables NAT 패킷 카운터 조회
docker exec <워커노드이름> sh -c 'nsenter -t $(pidof <프로세스이름>) -n iptables -t nat -L -n -v'
Step 8. 셀프 체크 (Self-Check Q&A)
Q1. 내가 작성한 YAML 파일에는 istio-init이나 istio-proxy가 없었는데, 쿠버네티스의 어떤 기능 덕분에 파드에 자동으로 추가되었나요?
쿠버네티스 API 서버의 MutatingAdmissionWebhook(변이 어드미션 웹훅) 기능 덕분입니다. 파드가 생성되기 직전 API 서버가 istio-sidecar-injector 웹훅을 통해 istiod에게 파드 명세를 보내면, istiod가 YAML에 istio-init(초기화 컨테이너)과 istio-proxy(사이드카 컨테이너)를 끼워 넣어 돌려줍니다.
Q2. study-client 안에서 curl(UID 100)이 쏜 패킷은 15001번 포트로 납치되는데, 같은 파드 안의 Envoy(istio-proxy)가 밖으로 쏘는 패킷은 왜 또 15001번으로 납치되지 않고 무사히 빠져나가나요?
Envoy 프로세스는 UID 1337 번으로 실행되며, ISTIO_OUTPUT 체인에 -m owner --uid-owner 1337 -j RETURN 규칙이 있어서 UID가 1337번인 프로세스가 만든 패킷은 납치(REDIRECT 15001)되기 전에 먼저 체인을 탈출(RETURN)하기 때문입니다.
Q3. order-api 파드의 Envoy(15006번 포트)가 외부 요청을 받아 mTLS 복호화를 마친 뒤, 같은 파드 안의 Node.js(8080번 포트)로 패킷을 전달할 때 왜 출발지 IP를 127.0.0.1이 아닌 127.0.0.6으로 바꿔서 보내나요?
만약 출발지 IP를 일반 IP로 두고 루프백(lo)을 통해 8080번 포트로 보내면, ISTIO_OUTPUT 체인의 "Envoy(UID 1337)가 루프백을 통해 자기 파드의 다른 포트로 보낼 때는 다시 15006으로 보내라"는 규칙에 걸려 무한 루프가 발생합니다. 이를 구분하기 위해 Envoy는 "이미 15006 검문을 통과해 내부 앱으로 넘기는 패킷" 에만 특수 출발지 IP인 127.0.0.6 을 부여하고, ISTIO_OUTPUT 체인 첫 줄(-s 127.0.0.6/32 -o lo -j RETURN)에서 이를 즉시 통과시킵니다.
🔗 다음 파트로 이동
1세대 사이드카 메시가 리눅스 커널 안에서 어떻게 동작하는지, 그리고 왜 파드마다 istio-init과 Envoy를 띄우는 구조가 무거울 수밖에 없는지 커널 카운터로 직접 확인했습니다.
다음 Part 03에서는 이 사이드카를 파드에서 완전히 들어내고, 노드당 단 1개의 초경량 Rust 프록시(ztunnel)와 HBONE(:15008) 터널링으로 동작하는 2세대 Ambient Mesh 로 클러스터를 직접 업그레이드해 봅니다!
네비게이션
| 이전 | 목록 | 다음 |
|---|---|---|
| ◀ Part 02 이론 | 목록으로 | Part 03 이론 ▶ |